/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mapbox.ohos.gestures;

import com.mapbox.ohos.gestures.ohan.PointF;

import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.multimodalinput.event.TouchEvent;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static com.mapbox.ohos.gestures.OhosGesturesManager.GESTURE_TYPE_MOVE;

/**
 * Gesture detector handling move gesture.
 * <p>
 * {@link MoveGestureDetector} serves similar purpose to
 * #onScroll(MotionEvent, MotionEvent, float, float)}, however, it's a {@link ProgressiveGesture} that
 * introduces {@link OnMoveGestureListener#onMoveBegin(MoveGestureDetector)},
 * {@link OnMoveGestureListener#onMoveEnd(MoveGestureDetector, float, float)},
 * threshold with {@link MoveGestureDetector#setMoveThreshold(float)} and multi finger support thanks to
 * {@link MoveDistancesObject}.
 */
public class MoveGestureDetector extends ProgressiveGesture<MoveGestureDetector.OnMoveGestureListener> {
    private static final int MOVE_REQUIRED_POINTERS_COUNT = 1;
    private static final Set<Integer> HANDLED_TYPES = new HashSet<>();
    private PointF previousFocalPoint;
    private boolean resetFocal;
    float lastDistanceX;
    float lastDistanceY;

    static {
        HANDLED_TYPES.add(GESTURE_TYPE_MOVE);
    }


    private RectFloat moveThresholdRect;
    private float moveThreshold;

    private final Map<Integer, MoveDistancesObject> moveDistancesObjectMap = new HashMap<>();

    public MoveGestureDetector(Context context, OhosGesturesManager gesturesManager) {
        super(context, gesturesManager);
    }

    @Override
    protected Set<Integer> provideHandledTypes() {
        return HANDLED_TYPES;
    }

    /**
     * OnMoveGestureListener
     */
    public interface OnMoveGestureListener {
        /**
         * Indicates that the move gesture started.
         *
         * @param detector this detector
         * @return true if you want to receive subsequent {@link #onMove(MoveGestureDetector, float, float)} callbacks,
         * false if you want to ignore this gesture.
         */
        boolean onMoveBegin(MoveGestureDetector detector);

        /**
         * Called for every move change during the gesture.
         *
         * @param detector this detector
         * @param distanceX X distance of the focal point in pixel since last call
         * @param distanceY Y distance of the focal point in pixel since last call
         * @return true if the gesture was handled, false otherwise
         */
        boolean onMove(MoveGestureDetector detector, float distanceX, float distanceY);

        /**
         * Indicates that the move gesture ended.
         *
         * @param velocityX velocityX of the gesture in the moment of lifting the fingers
         * @param velocityY velocityY of the gesture in the moment of lifting the fingers
         * @param detector this detector
         */
        void onMoveEnd(MoveGestureDetector detector, float velocityX, float velocityY);
    }

    /**
     * SimpleOnMoveGestureListener
     */
    public static class SimpleOnMoveGestureListener implements OnMoveGestureListener {

        @Override
        public boolean onMoveBegin(MoveGestureDetector detector) {
            return true;
        }

        @Override
        public boolean onMove(MoveGestureDetector detector, float distanceX, float distanceY) {
            return false;
        }

        @Override
        public void onMoveEnd(MoveGestureDetector detector, float velocityX, float velocityY) {
            // No implementation
        }
    }

    @Override
    protected boolean analyzeEvent(TouchEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:
            case TouchEvent.OTHER_POINT_DOWN:
                resetFocal = true;

                float pX = motionEvent.getPointerPosition(motionEvent.getIndex()).getX();
                float pY = motionEvent.getPointerPosition(motionEvent.getIndex()).getY();
                moveDistancesObjectMap.put(
                    motionEvent.getPointerId(motionEvent.getIndex()),
                    new MoveDistancesObject(pX, pY)
                );
                break;

            case TouchEvent.OTHER_POINT_UP:
                moveDistancesObjectMap.clear();
                break;

            case TouchEvent.PRIMARY_POINT_UP:
                resetFocal = true;

                moveDistancesObjectMap.remove(motionEvent.getPointerId(motionEvent.getIndex()));
                break;

            case TouchEvent.CANCEL:
                moveDistancesObjectMap.clear();
                break;

            default:
                break;
        }
        if (TouchEvent.PHASE_MOVE == motionEvent.getAction()) {
        } else {

        }
        return super.analyzeEvent(motionEvent);
    }

    @Override
    protected boolean analyzeMovement() {
        super.analyzeMovement();
        updateMoveDistancesObjects();
        if (isInProgress()) {
            PointF currentFocalPoint = getFocalPoint();
            lastDistanceX = previousFocalPoint.pointX - currentFocalPoint.pointX;
            lastDistanceY = previousFocalPoint.pointY - currentFocalPoint.pointY;
            previousFocalPoint = currentFocalPoint;
            if (resetFocal) {
                resetFocal = false;
                return listener.onMove(this, 0, 0);
            }
            return listener.onMove(this, lastDistanceX, lastDistanceY);
        } else if (canExecute(GESTURE_TYPE_MOVE)) {
            if (listener.onMoveBegin(this)) {
                gestureStarted();
                previousFocalPoint = getFocalPoint();
                resetFocal = false;
                return true;
            }
        }
        return false;
    }

    private void updateMoveDistancesObjects() {
        for (int pointerId : pointerIdList) {
            try {
                if (moveDistancesObjectMap.get(pointerId) != null) {
                    moveDistancesObjectMap.get(pointerId).addNewPosition(
                        getCurrentEvent().getPointerPosition(pointerId).getX(),
                        getCurrentEvent().getPointerPosition(pointerId).getY()
                    );
                }
            } catch (Exception exception) {
                return;
            }

        }
    }

    boolean checkAnyMoveAboveThreshold() {
        for (MoveDistancesObject moveDistancesObject : moveDistancesObjectMap.values()) {
            boolean thresholdExceeded = Math.abs(moveDistancesObject.getDistanceXSinceStart()) >= moveThreshold
                || Math.abs(moveDistancesObject.getDistanceYSinceStart()) >= moveThreshold;

            boolean isInRect = moveThresholdRect != null && moveThresholdRect.isInclude(getFocalPoint().pointX, getFocalPoint().pointY);
            return !isInRect && thresholdExceeded;
        }
        return false;
    }

    @Override
    protected boolean canExecute(int invokedGestureType) {
        return super.canExecute(invokedGestureType) && checkAnyMoveAboveThreshold();
    }

    @Override
    protected void reset() {
        super.reset();
    }

    @Override
    protected void gestureStopped() {
        super.gestureStopped();
        listener.onMoveEnd(this, velocityX, velocityY);
    }

    @Override
    protected int getRequiredPointersCount() {
        return MOVE_REQUIRED_POINTERS_COUNT;
    }

    /**
     * Get the delta pixel threshold required to qualify it as a move gesture.
     *
     * @return delta pixel threshold
     * @see #getMoveThresholdRect()
     */
    public float getMoveThreshold() {
        return moveThreshold;
    }

    /**
     * Set the delta pixel threshold required to qualify it as a move gesture.
     * <p>
     * We encourage to set those values from dimens to accommodate for various screen sizes.
     *
     * @param moveThreshold delta threshold
     */
    public void setMoveThreshold(float moveThreshold) {
        this.moveThreshold = moveThreshold;
    }

    /**
     * Get the screen area in which the move gesture cannot be started.
     * If the gesture is already in progress, this value is ignored.
     * This condition is evaluated before {@link #setMoveThreshold(float)}.
     *
     * @return the screen area in which the gesture cannot be started
     */
    public RectFloat getMoveThresholdRect() {
        return moveThresholdRect;
    }

    /**
     * Set the screen area in which the move gesture cannot be started.
     * If the gesture is already in progress, this value is ignored.
     * This condition is evaluated before {@link #setMoveThreshold(float)}.
     *
     * @param moveThresholdRect the screen area in which the gesture cannot be started
     */
    public void setMoveThresholdRect(RectFloat moveThresholdRect) {
        this.moveThresholdRect = moveThresholdRect;
    }

    /**
     * Set the delta dp threshold required to qualify it as a move gesture.
     *
     * @param moveThresholdDimen delta threshold
     */
    public void setMoveThresholdResource(int moveThresholdDimen) {
        setMoveThreshold(Utils.fpToPx(moveThresholdDimen, context));
    }

    /**
     * Returns X distance of the focal point in pixels
     * calculated during the last {@link OnMoveGestureListener#onMove(MoveGestureDetector, float, float)} call.
     *
     * @return X distance of the focal point in pixel
     */
    public float getLastDistanceX() {
        return lastDistanceX;
    }

    /**
     * Returns Y distance of the focal point in pixels
     * calculated during the last {@link OnMoveGestureListener#onMove(MoveGestureDetector, float, float)} call.
     *
     * @return Y distance of the focal point in pixel
     */
    public float getLastDistanceY() {
        return lastDistanceY;
    }

    /**
     * Returns {@link MoveDistancesObject} referencing the pointer held under passed index.
     * <p>
     * Pointers are sorted by the time they were placed on the screen until lifted up.
     * This means that index 0 will reflect the oldest added, still active pointer
     * and index ({@link #getPointersCount()} - 1) will reflect the latest added, still active pointer.
     * <p>
     *
     * @param pointerIndex pointer's index
     * @return distances object of the referenced pointer
     */
    public MoveDistancesObject getMoveObject(int pointerIndex) {
        if (isInProgress()) {
            if (pointerIndex >= 0 && pointerIndex < getPointersCount()) {
                return moveDistancesObjectMap.get(pointerIdList.get(pointerIndex));
            }
        }
        return null;
    }
}
